feat(android): support ActivityResultContracts in native modules - #57798
feat(android): support ActivityResultContracts in native modules#57798matinzd wants to merge 7 commits into
Conversation
…ntext Adds com.facebook.react.activityresult with a ReactActivityResultCaller that registers AndroidX ActivityResultContracts against the host Activity's own ActivityResultRegistry (ReactActivity already extends ComponentActivity, so it is an ActivityResultRegistryOwner). No changes to consumers' MainActivity, no manifest entries, no forked registry. - Registration is legal at any time: modules are created lazily, so the returned launcher is a deferred wrapper that binds to the registry on onHostResume, queues a single launch issued while unbound, and rebinds under the same key after Activity recreation. - Keys are the contract's fully-qualified class name; duplicate registrations throw at registration time, with an owner-scoped overload as the escape hatch. - ReactContext gains registerForActivityResult convenience methods mirroring ComponentActivity, plus getActivityResultCaller(). - ActivityEventListener dispatch is untouched; results flow through ComponentActivity's existing onActivityResult / onRequestPermissionsResult into its registry. Demo: SampleTurboModule.requestSamplePermission (CAMERA) wired into rn-tester's SampleTurboModuleExample under an Android-only section. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
This is my decisions and how I planned implementing this with Claude. Comments are welcome! https://gist.github.com/matinzd/5cac3ef1811efc9817f055b8d27692f7 |
|
Not sure how to update |
|
@matinzd we can generate it after importing, I don't believe there's a public Gradle script for it |
Yep. I saw the command in other PRs using buck2! The PR is now ready to review. Can you please approve the jobs to run? |
|
Thanks for tackling this @matinzd, the ActivityResultContract gap is real and Health Connect is a good example. A few things I think are worth sorting first, mostly around lifecycle and threading, though I might be missing some context:
On testing, it looks like the current coverage is mostly the sample module wiring, so the lifecycle and threading paths above are not really exercised. It would help to add tests for those paths, mainly the rebind case (a launch after an Activity swap should hit the new registry) and the main-thread requirement for register and launch. |
|
Thanks for the review @fabriziocucci!
Sorry, that slipped through. Claude made a mistake, and it’s good that you caught it. I’m thinking of deriving the key from the FQCN of the native module combined with the contract FQCN. We could throw an error if the same contract is registered twice from the same module, prompting users to use the overload that accepts a caller-provided key instead. E.g: // Throws: MyModule already registered a launcher for androidx...GetContent.
private val pickAvatar = ctx.registerForActivityResult(this, GetContent()) { }
private val pickBanner = ctx.registerForActivityResult(this, GetContent()) { }
// Fix:
private val pickAvatar = ctx.registerForActivityResult("avatar", GetContent()) { }
private val pickBanner = ctx.registerForActivityResult("banner", GetContent()) { }or we can just silently add an index based key based on each module to avoid collision for e.g: // key = "com.some.image.lib.ImageModule:androidx...GetContent#0"
// key = "com.some.image.lib.ImageModule:androidx...GetContent#1"What do you think? (Not sure who to tag though from the core team)
For the rebind case, I tried adding a For the rest, I need to create an example app in order to test those cases. I will let you know when that's ready. |
Summary:
Rendered readme can be found here.
Bare React Native has no way for a native module to use AndroidX
ActivityResultContracts. Modules are stuck withActivityEventListenerand self-assigned int request codes. On Android 14+ some contracts (e.g. Health Connect's permission contract) produce a synthetic intent that only anActivityResultRegistrycan service, so the classicstartActivityForResultpath fails withActivityNotFoundExceptionoutright.Libraries work around this by demanding glue code in the consumer's
MainActivity(e.g.HealthConnectPermissionDelegate.setPermissionDelegate(this)) or by shipping a transparentActivityin their manifest, which cuts against Google's single-activity guidance (matinzd/react-native-health-connect#266, #33639, #36377). Expo solved this withregisterActivityContracts; bare RN has no equivalent.ReactActivityalready extendsComponentActivity, so it already owns a realActivityResultRegistryand routes results into it. Core just needs to hand modules a path to that registry:Design notes:
ComponentActivity.registerForActivityResultand returns the realandroidx.activity.result.ActivityResultLauncher<I>.onHostResume, queues alaunch()issued while unbound, and rebinds under the same key after Activity recreation.ReactActivity/ReactActivityDelegate/ReactDelegate, no new Gradle dependency, no manifest changes, no forked registry.ActivityEventListeneris untouched.Promise) died with the JS context.Demo:
SampleTurboModule.requestSamplePermission()(CAMERA), surfaced in rn-tester's SampleTurboModule screen under an Android-only section.Changelog:
[ANDROID] [ADDED] - Native modules can register AndroidX
ActivityResultContracts viaReactContext.registerForActivityResult, with no changes to the consumer'sMainActivityTest Plan:
./gradlew :packages:react-native:ReactAndroid:compileDebugKotlinpasses; codegen emitsrequestSamplePermissionintoNativeSampleTurboModuleSpec.requestSamplePermission:true; deny or back resolvesfalseExample App Recording
Screen.Recording.2026-08-03.at.15.27.08.mov